Skip to content

feat(payments): switch-provider recovery flow + inline guided wizard (#43 US3+US4)#64

Open
TortoiseWolfe wants to merge 2 commits intomainfrom
043/payment-recovery-flow
Open

feat(payments): switch-provider recovery flow + inline guided wizard (#43 US3+US4)#64
TortoiseWolfe wants to merge 2 commits intomainfrom
043/payment-recovery-flow

Conversation

@TortoiseWolfe
Copy link
Copy Markdown
Owner

Summary

Closes the remaining 2 of 7 gaps in #43 (US3 + US4) — reframed for ScriptHammer's static-export architecture.

PR #62 corrected the false 'route missing' framing of the original issue. PR #63 closed gaps 1-5 (idempotency-key reuse, attempt counter + cooling, error categorization, offline banner, audit log). This PR closes gaps 6-7 with two architecture-fit reframings recorded in the spec amendment commit.

US3 reframed — "Switch Payment Method" (FR-011-FR-015)

The spec's literal 'Update Payment Method' assumes saved cards + stripe.js Elements + a PCI surface. ScriptHammer never stores cards — every checkout is a fresh Stripe Checkout (or PayPal redirect, or Cash App / Chime direct link). The honest interpretation in this codebase is: after a card decline, let the user pick a different provider.

New `` 5-file component:

US4 reframed — Inline progressive disclosure (FR-016-FR-019)

Replaces a dedicated wizard component with structure inside the failed-state block of `` that escalates with `retry_count`:

retry_count UI
0 Retry button + 'Use a different payment method' button only
1 + collapsed `
Details`: 'Need more help?'
2 + expanded recovery list: (1) Try again (2) Switch method (3) Contact support
3 (cap) Step 1 struck through, step 3 emphasized — escalate to support (FR-019)

Honors FR-016 (guided steps), FR-017 (prioritized: retry → switch → support), FR-018 (alternative payment options), FR-019 (escalation).

Commits

  • `90de708` — service + audit + spec extensions (~6 files, 164 LOC). Doesn't change user-visible behavior on its own.
  • `851ec99` — UI surface: SwitchProviderPanel + PaymentStatusDisplay restructure + e2e (~8 files, 718 LOC).

Out of scope

Test plan

E2E — #53 progress

Two new `test.skip` stubs added with explicit comments pointing to the unit tests that exercise the same logic. They follow the same Stripe-API-keys gate as the other Checkout-driven skips. The unit-level coverage is the load-bearing test surface for these flows.

Sharp edges

🤖 Generated with Claude Code

TurtleWolfe and others added 2 commits April 28, 2026 19:43
 US3+US4)

Foundation for the SwitchProviderPanel UI in the next commit. Doesn't
change any user-visible behavior on its own — adds the data plumbing
that lets the UI mount a fresh PaymentButton from a previously-failed
parent intent and preserve the audit chain across providers.

Service surface:
- createPaymentIntent gains optional parent_intent_id arg. When set, the
  INSERT carries it through to the payment_intents row so the parent_intent_id
  column shipped in PR #63 actually gets populated for cross-provider retries
  (not just same-provider retries via retryFailedPayment).
- New getParentIntentForRetry(intentId) returns { amount, currency, type,
  interval, customer_email, description, retry_count } — the fields needed
  to seed PaymentButton. Mirrors retryFailedPayment's server-side guards
  (PaymentRetryLimitError + PaymentRetryExpiredError) so the recovery panel
  fails fast before mounting. Cooling is intentionally NOT enforced here —
  switching providers does not reuse the parent's idempotency_key, so cooling
  protects against nothing in this path.
- usePaymentButton accepts a parentIntentId option; plumbed through to
  createPaymentIntent.

Audit shape extensions (non-breaking — event_data is JSONB):
- recovery_method: 'same_provider' | 'switch_provider' (defaults to
  same_provider so existing call-sites stay unchanged)
- selected_provider: PaymentProvider (set when switching)
Admin dashboard's audit reader already shows raw event_data so the new
fields surface for free.

Spec amendment (features/payments/040-payment-retry-ui/spec.md):
- Status flipped to "Mostly Shipped (recovery UX shipped; saved-method
  storage out of scope)"
- US3 reframed: "switch payment method (provider switch)" replaces
  literal "Update Payment Method (saved cards)". ScriptHammer never
  stores cards — every checkout is a fresh provider session — so the
  honest interpretation in this codebase is "after a card decline, let
  the user pick a different provider". Saved-card storage (Stripe Customer
  + saved_payment_methods + stripe.js Elements) is a separate multi-PR
  feature behind a PCI scope review.
- US4 reframed: inline progressive disclosure replaces a dedicated wizard
  component. The recovery list escalates with retry_count.
- Both reframings preserve the user-facing intent of FR-011-FR-019.

Tests: 4 new in retry.test.ts (getParentIntentForRetry happy path, limit
guard, expired guard, no-cooling-guard semantics). All 24 retry tests
green, full suite 3269/3269 across 288 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-result (#43 US3+US4)

Closes the remaining 2 of 7 gaps in #43, reframed for ScriptHammer's
static-export architecture (saved-card storage is out of scope; honest
interpretation is "switch payment method (provider)" + "guided escalation
via progressive disclosure").

US3 — SwitchProviderPanel (5-file component):
- Reads parent intent via getParentIntentForRetry, seeds PaymentButton
  pre-filled with parent's amount + currency + type + email + description.
- Reuses PaymentButton's multi-provider machinery (Stripe / PayPal /
  Cash App / Chime), GDPR consent gating per provider, and offline queue.
  Nothing rebuilt.
- New intent links to parent via parent_intent_id so the audit chain
  spans providers (matches schema added in PR #63).
- Handles limit-reached + expired + generic-error states; shows masked
  "switching from previous attempt: $X" callout with the parent's
  formatted amount + description.
- 8 unit tests + 1 a11y test.

US4 — Inline progressive recovery disclosure on PaymentStatusDisplay:
- retry_count = 0: just the retry button (existing behavior).
- retry_count = 1: collapsed <details>: "Need more help?" hint.
- retry_count >= 2: <details open> showing the recovery list:
    1. Try again — payment failures are sometimes temporary.
    2. Use a different payment method (Stripe / PayPal / Cash App / Chime).
    3. Contact support with the transaction reference above.
- retry_count >= 3 (cap): step 1 is struck through; step 3 is bold —
  emphasizes support contact (FR-019).
- Honors FR-016 (guided steps), FR-017 (prioritized: retry → switch →
  support), FR-018 (alternative payment options), FR-019 (escalation).
- No new component file for the wizard — structure inside the existing
  failed-state block, since the categorization + counter + cooling shipped
  in PR #63 is the focal point.
- 7 new unit tests for switch button visibility, panel toggle, recovery
  disclosure escalation across retry_count values.

UI restructure of failed-state block (PaymentStatusDisplay):
- Counter + retry button + "Use a different payment method" button on one
  row (flex-wrap on mobile).
- Switch button has aria-expanded + aria-controls bound to the panel.
- Inline panel mount below the action row.
- Recovery disclosure below that.

E2E (tests/e2e/payment/03-failed-payment-retry.spec.ts):
- Two new test.skip stubs documenting the Stripe-Checkout gate. Both
  scenarios are exercised by unit tests; the e2e versions need a real
  failed payment_results row that requires Stripe API keys to produce.
  Same gate as the other Stripe-Checkout skips in this file.

Verification:
- pnpm test: 3269/3269 across 288 files (20 new for this PR)
- pnpm test:rls: 55/55 (no schema changes; sanity)
- pnpm run type-check, lint: clean

#43 still open after this PR — gaps 1-7 closed in spirit, but the literal
"saved cards" interpretation of FR-011-FR-015 stays out of scope and the
issue tracks any future architectural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Gap-Audit] 040 Payment Retry UI: idempotency-key reuse, retry counter + cooling, error categorization, offline banner, audit log

2 participants